# -*- coding: utf-8 -*-

import sys
import warnings
import heapq
from heapq import heappop, heappush

import pygame
from pygame.time import get_ticks

#  -----------------------------------------------------------------------------

class Scheduler(object):
    """
    This is a scheduler that works on a frame basis. Call update() on each
    frame and all scheduled methods (== callbacks) will be called.

    The return value of the callback decides if the callback is re-scheduled.
    If a callback returns 0 the callback will be removed and no further
    re-scheduling is done (the callback is called once).

    To re-schedule the callback it should return a integer number describing
    the next frame it should be called back.

    ::

        def callback(*args, **kwargs):
            # code
            return 10 # this callback is called in 10 frames again

    """

    STOPREPEAT = 0

    def __init__(self):
        self._heap = []
        heapq.heapify(self._heap)
        self._nexttime = sys.maxsize
        self._current_time = 0

    def schedule(self, func, interval, offset=0, *args, **kwargs):
        """
        Schedule a function or method to be called back a time later.

        :param func: function or method to call back in the future
        :type func: function of method reference
        :param interval: time that should pass until next call of func,
            has to be >= 0 (a value of 0 means it is called next time
            the update method is called)
        :type interval: int (Scheduler) or float (Scheduler)
        :param offset: the start offset. This helps to distribute repeating
            callbacks evenly over time manually to prevent a high number
            of callbacks in a single frame.
        :type offset:  int (Scheduler) or float (Scheduler)
        :param args: arguments of the scheduled function
        :type args: list
        :param kwargs: keyword arguments of the scheduled function
        :type kwargs: dict

        """
        assert interval >= 0
        nex_time = self._current_time + interval + offset
        heappush(self._heap, (nex_time, interval, func, args, kwargs))
        if nex_time < self._nexttime:
            self._nexttime = nex_time

    def update(self, clock, delta_time, current_time, *args, **kwargs):
        """
        """
        self._current_time = current_time
        assert self._current_time < sys.maxsize # can this ever happend?
        heap = self._heap
        while self._nexttime <= self._current_time:
            next_time, oldinterval, func, args, kwargs = heappop(heap)

            # interval = func(clock, delta_time, current_time, *args, **kwargs)
            interval = func(clock, oldinterval, next_time, *args, **kwargs)
            if isinstance(clock, LockStepClock):
                print '!!!!', delta_time, current_time, next_time, self._nexttime

            # TODO: maybe use 0.0 to schedule in next update and a negative number for stop scheduling instead?
            if interval > 0.0:
                # TODO: performance of heap?
                # heappush(heap, (self._current_time + interval, func, args, kwargs))
                heappush(heap, (next_time + interval, interval, func, args, kwargs))

            # TODO: how to remove the sys.maxsize??
            if heap:
                self._nexttime = heap[0][0]
            else:
                # no entry in heap
                self._nexttime = sys.maxsize

    def clear(self):
        """
        removes all callbacks.
        """
        self._heap[:] = []
        self._check_next()

    def remove(self, func, all_occurences=False):
        """
        Remove a callback function.

        :param func: the function to remove, does nothing if functions is not
            in schedule
        :type func: function or method reference
        :param all_occurences: if False (default) only the first occurence
            of the function is removed, if True all occurences in the schedule
            are removed
        :type all_occurences: bool
        """
        to_remove = []
        for entry in self._heap:
            nex_time, interval, entry_func, args, kwargs = entry
            if entry_func == func:
                to_remove.append(entry)
                if not all_occurences:
                    break
        for entry in to_remove:
            self._heap.remove(entry)
        self._check_next()

    def _check_next(self):
        """
        Checks when the next callback should be called.
        """
        if self._heap:
            self._nexttime = self._heap[0][0]
        else:
            self._nexttime = sys.maxsize

#  -----------------------------------------------------------------------------

class Timer(object):
    """
    Timer is a convenience class to use a :class:`Scheduler` or one of its
    derived classes. By default it uses a :class:`Scheduler`. If
    another scheduler should be used, assign another instance of a
    Scheduler to Timer.scheduler *before* **any** use of the Timer class.

    Once a frame you need to call the schedulers update method:

    ::

        # once a frame in your mainloop
        Timer.scheduler.update(delta_time)

    You could assing different schedulers to instances of the Timer class
    by overriding the class attribute scheduler with an instance attribute

    ::

        my_scheduler = Scheduler()
        timer = Timer(10)
        timer.scheduler = my_scheduler

    Don't forget to update the used scheduler once per frame.

    The timer class has an event_elapsed attribute which is a
    :class:`events.Signal` instance. It is used like this:

    ::

        # my_callback should have following signature
        def my_callback(self, timer_that_fired):
            pass

        # register my_callback at the timer
        timer = Timer(0.5, False)
        timer.event_elapsed += my_callback



    :classattr scheduler: a Scheduler or a derived class

    :attr intervall: the intervall to be used
    :attr repeat: repeated firing of the elapsed event

    :param intervall: the time to wait until the event_elapsed is fired
    :type intervall: depends what the scheduler uses, defaults to a float
        representing seconds
    :param repeat: defaults to False, if the elapsed event should be fired
        repeatadly with the given intervall
    :type repeat: bool

    """

    scheduler = Scheduler()

    def __init__(self, intervall, repeat=False):
        """
        Constructor.
        """
        self.intervall = intervall
        self.repeat = repeat
        self.event_elapsed = Signal()
        self._is_running = False

    def start(self, repeat=None):
        """
        Starts the timer to fire the elapsed events (internally: it schedules
        a callback method to the scheduler).

        :param repeat: defaults to None, overwrites the repeat attribute if
            set to a boolean value
        :type repeat: bool
        """
        self._is_running = True
        self.scheduler.schedule(self._callback, self.intervall)
        if repeat is not None:
            self.repeat = repeat

    def stop(self):
        """
        Stops the timer from firing the elapsed event (internally: it removes
        all callbacks from the scheduler).
        """
        self._is_running = False
        self.scheduler.remove(self._callback, True)

    def _callback(self, *args, **kwargs):
        """
        Callback method that gets called from the scheduler.
        """
        self.event_elapsed(self)
        if self.repeat:
            return self.intervall
        else:
            return 0

    # def __del__(self):
        ## if Timer.scheduler would use weakrefs this would be useful
        # self.stop()
        # object.__del__(self)

#  -----------------------------------------------------------------------------


import types
from weakref import ref # pragma: no cover

#  -----------------------------------------------------------------------------
HANDLED = True
UNHANDLED = False
NEW_FIRST = 0
NEW_LAST = -1
USE_EVENT_TYPE_ORDER = None

class Signal(object):
    """
    A signal object. It is for event dispatching. It saves a list of
    observers and when fired, it calls the observers. The handler method
    has to accept the exact same number of arguments that are given to
    the fire method. Or it should make use of the args and kwargs arguments.
    The return type of a handler is important. When returning True or any value
    that evaluates to True in a if statement, the signal stops calling further
    observers. Returning False or None (as default behavior of methods without a
    return), it will call further handler methods.

    The order in which the handler methods will be called can be defined
    when the signal is instanciated. The sort order can only have one of
    these values:

     * Signal.`NEW_FIRST`
     * Signal.`NEW_LAST`


    Default is NEW_FIRST. This means that handlers added later are called before
    the older ones::

       s = Signal()
       s.add(handler1)
       s.add(handler2)
       s.fire(sender) # call order: handler2, handler1

    This is important and useful, when imitating a push behavior. If handler2
    returns a `HANDLED` then handler1 wont get be called.

    Using the `NEW_LAST` order for event handler leads to this::

       s = Signal()
       s.add(handler1)
       s.add(handler2)
       s.fire(sender) # call order: handler1, handler2

    This is useful for a draw event. A convinient way to draw is in a back to
    front manner. Adding first the background and then the other things on top
    will draw the things in the correct order.

    This Signal implementation is iteration safe. It is possible to remove
    the handler from within the handler without getting in trouble.

    :Warning:
        Any class that has a method added will be kept alive by this reference.
        To allow the class to die it has to remove the handler. See `WeakSignal`
        for a implementation using weak references.

    :Ivariables:
        name : string
            Either the string that was given or None.
        enabled : bool
            If this Signal fires or not.
        cur_event_list : list
            current list of methods to call during firing. Only used if chaining
            signals.

    :Cvariables:
        NEW_FIRST : int
            Used to define the call order. New handlers are added in front of
            the others and therefore called before the others.
        NEW_LAST : int
            Used to define the call order. New handlers are appended
            and therefore called after the others.

    :Note:
        If you get trouble with the recursion limit then try to avoid the
        shortcuts::

            sig += sig2 # use sig += sig2.fire instead
            sig(sender, args)   # use sig.fire(args) instead

        This should double the available recursion depth due to less function
        calls done.

    """

    _ADD, _RMV, _CLR = list(range(3))


    def __init__(self, name=None, sort_order = NEW_FIRST):
        """
        Constructor.

        :Parameters:
            name : string
                Optional. A name to identify the signal easier when debugging.
            sort_order : Constant
                Either NEW_FIRST or NEW_LAST. Defines the order in
                which the handler are called. Defaults to NEW_FIRST.

        """
        #-- public --#
        if name:
            self.name = name
        else:
            self.name = hex(id(self))
        self.enabled = True
        #-- protected --#
        # observers
        self._observers = []
        # sortorder
        self._sort_oder = sort_order
        # commands
        self._changed = False
        self._commands = []

    def add(self, obs):
        """
        Adds a handler to the signal.

        :Note:
            Shortcut::

                sig += obs

        :Parameters:
            obs : callable
                A handler to add, has to be callable.
        """
        self._changed = True
        self._commands.append((self._ADD, obs))
        return self

    def remove(self, obs):
        """
        Removes a handler.

        :Note:
            Shortcut::

                sig -= obs

        :Parameters:
            obs : callable
                The handler to be removed.
        """
        self._changed = True
        self._commands.append((self._RMV, obs))
        return self

    def fire(self, *args, **kwargs):
        """
        Fires the signal with any arguments.

        :Note:
            Shortcut::

                sig(sender, *args, **kwargs)

            Be aware of recursion, see class introduction.

        :Parameters:
            sender : object
                The sender of this event.
            args : args
                Arguments list.
            kwargs : kwargs
                Named arguments, a dict.

        :rtype: True when a handler returns `HANDLED` , else False
        """
        if self.enabled:
            if self._changed:
                self._sync()
            for observer in self._observers:
                if observer(*args, **kwargs):
                    return HANDLED
        return UNHANDLED

    def clear(self):
        """
        Removes all handlers from the signal.
        """
        self._commands.append((self._CLR, []))

    def _sync(self):
        """
        Only used internaly. This method is for synchronizing
        the added or removed observers.
        """
        self._changed = False
        while self._commands:
            cmd, obs = self._commands.pop(0)
            if cmd == self._ADD and obs not in self._observers:
                if self._sort_oder == NEW_FIRST:
                    self._observers.insert(0, obs) # start
                else:
                    self._observers.append(obs) # end
            elif cmd == self._RMV:
                if obs in self._observers:
                    self._observers.remove(obs)
            elif cmd == self._CLR:
                self._observers = []
                self._commands = []

    def __len__(self):
        """
        Returns the number of handlers. Schould not be called from within a
        handler of this event.
        """
        self._sync()
        return len(self._observers)

    def __str__(self):
        return '<%s(\'%s\') %s at %s>' % (self.__class__.__name__,
                                       self.name,
                                       self._observers,
                                       hex(id(self)))

    # convinients (but slow)
    __iadd__ = add
    __isub__ = remove
    __call__ = fire

#  -----------------------------------------------------------------------------


class FrameClock(object):

    def __init__(self):
        self._frame_count = 0
        self.on_tick = Signal("FrameClock.on_tick signal", sort_order=NEW_LAST)
        
    def update(self):
        self._frame_count += 1
        # 'dt' is always 1
        self.on_tick.fire(self, 1, self._frame_count)
        
    @property
    def frame_count(self):
        return self._frame_count
        
#  -----------------------------------------------------------------------------

class RealTimeClock(object):

    def __init__(self, get_ticks):
        self.on_tick = Signal("RealTimeClock.on_tick signal", sort_order=NEW_LAST)
        self._real_time = 0
        self._get_ticks = get_ticks

    def update(self, frame_clock, *args, **kwargs):
        dt = self._get_ticks() - self._real_time
        self._real_time += dt
        self.on_tick.fire(self, dt, self._real_time)

#  -----------------------------------------------------------------------------

class GameTimeClock(object):
    
    def __init__(self, dt_max):
        self.on_tick = Signal("GameTimeClock.on_tick signal", sort_order=NEW_LAST)
        self._game_time = 0
        self._dilatation = 1.0
        # TODO: really use dt max in this clock? or later in the chain?
        # the max time changes the 'real' part of the time
        self._dt_max = dt_max

    def update(self, clock, dt, real_time, *args, **kwargs):
        # TODO: eventually move to other clock
        if dt > self._dt_max:
            dt = self._dt_max

        gdt = self._dilatation * dt
        self._game_time += gdt
        self.on_tick.fire(self, gdt, self._game_time)
        
    @property
    def dilatation(self):
        return self._dilatation

    @dilatation.setter
    def dilatation(self, value):
        self._dilatation = value

    @property
    def alpha(self):
        return 0.0

#  -----------------------------------------------------------------------------

class LockStepClock(object):

    def __init__(self, interval):
        self.on_tick = Signal("LockStepClock.on_tick signal", sort_order=NEW_LAST)
        self._update_interval = interval
        self._accumulator = 0
        self._game_time = 0
        self._alpha = 0.0
        
        
    def update(self, clock, gdt, game_time, *args, **kwargs):
        """
        Ticks the clock. Call this in you main loop on each frame as fast as
        possible. This will result in heigh cpu usage because it uses busy
        waiting!
        """

        # this would be the upper limit to prevent the 'spiral of death'
        # this is ~3 times faster than using min(...)
        # if frame_time > self._max_frame_time:
            # frame_time = self._max_frame_time

        # drive the simulation, integrate in fixed time steps
        self._accumulator += gdt
        # print '????', self._accumulator
        while self._accumulator >= self._update_interval:
            self._game_time += self._update_interval
            print '????', self._update_interval, self._game_time
            self.on_tick.fire(self, self._update_interval, self._game_time)
            self._accumulator -= self._update_interval
            
        self._alpha = float(self._accumulator) / self._update_interval
        assert self._alpha < 1, "alpha should be < 1.0 but it is: " + str(self._alpha)
    
    @property
    def alpha(self):
        return self._alpha
    
    
#  -----------------------------------------------------------------------------
import time
_start = time.time()
def get_time_ticks():
    t = (time.time() - _start) * 1000.0
    return t

frame_clock = FrameClock()
realtime_clock = RealTimeClock(get_ticks)
# realtime_clock = RealTimeClock(get_time_ticks)
gametime_clock = GameTimeClock(50) # 50 ms max for dt
lockstep_clock = LockStepClock(1000.0 / 30.0) # 30 fps 


frame_scheduler = Scheduler()
realtime_scheduler = Scheduler()
gametime_scheduler = Scheduler()
lockstep_scheduler = Scheduler()

frame_clock.on_tick.add(frame_scheduler.update)
realtime_clock.on_tick.add(realtime_scheduler.update)
gametime_clock.on_tick.add(gametime_scheduler.update)
lockstep_clock.on_tick.add(lockstep_scheduler.update)

frame_clock.on_tick.add(realtime_clock.update)
realtime_clock.on_tick.add(gametime_clock.update)
gametime_clock.on_tick.add(lockstep_clock.update)

# #  --timer example--
# # make the timers work with the lockstep time
# lockstep_clock.on_tick.add(Timer.scheduler.update)

# def timer_callback(timer):
    # print 'TIMER:', timer

# timer = Timer(1000, True)
# timer.event_elapsed.add(timer_callback)
# timer.start()
# #  --timer example--

# def update(*args, **kwargs):
    # print "update", args, kwargs
    
# def draw(alpha):
    # print "draw", alpha, pygame.time.get_ticks()
    
# draw_interval = 10  # 100 fps
# def draw_callback(*args, **kwargs):
    # # print 'draw_callback', args[1:], kwargs
    # # print 'lockstepclock', lockstep_clock._accumulator, lockstep_clock._game_time, lockstep_clock._alpha, pygame.time.get_ticks() - lockstep_clock._game_time

    # alpha = lockstep_clock.alpha
    # draw(alpha)
    # return draw_interval # re-schedule

# lockstep_clock.on_tick.add(update)
# realtime_scheduler.schedule(draw_callback, draw_interval)
    
    
# pygame.init()

# running = True
# while running:
    # frame_clock.update()
    # pygame.event.pump()
    # print '-----------------------------'


#  -----------------------------------------------------------------------------

if __name__ == '__main__':
    import sys
    import pygame
    from pygame.locals import *
    

    
    class Ball(pygame.sprite.Sprite):
        def __init__(self, name, color, y):
            pygame.sprite.Sprite.__init__(self)
            font = pygame.font.SysFont(None, 30)
            self.image = font.render(name, True, color)
            self.rect = self.image.get_rect(y=y)
            self.x = self.rect.x
            self.y = self.rect.y
            self.x0 = self.x
            self.speed = 120
            self.dir = 1
            self.time = time.time()
        def update(self, dt):
            screen_rect = pygame.display.get_surface().get_rect()
            new_x = self.x + self.speed * dt * self.dir
            self.rect.x = round(new_x)
            if screen_rect.contains(self.rect):
                self.x0 = self.x
                self.x = new_x
            else:
                self.dir *= -1
                self.rect.x = self.x0 = self.x
        def rect_interp(self, alpha):
            dx = self.x - self.x0
            ax = dx * alpha
            return self.rect.move(round(ax), 0)
    
    class Game(object):
        def __init__(self):
            self.screen = pygame.display.set_mode((640,480))
            self.screen_rect = self.screen.get_rect()
            self.font = pygame.font.SysFont(None, 18)
            
            self.which_clock = 3
            # if len(sys.argv) > 1:
                # self.which_clock = int(sys.argv[1])
            # if self.which_clock == 1:
                # print 'MULTI'
                # self.clocks = MultiClock()
                # self.clocks.frame_clock.schedule_item(self.tick)
                # self.lock_item = self.clocks.lockstep_clock.schedule_item(self.lockstep_update, 1/30.0)
            # elif self.which_clock == 2:
                # print 'NICER'
                # self.clocks = NicerClock()
                # self.lock_item = self.clocks.lockstep_clock.schedule_item(self.lockstep_update, 1/30.0)
            # elif self.which_clock == 3:
                # print 'NICE'
                # self.clocks = NiceClock()
            # self.game_item = self.clocks.game_clock.schedule_item(self.game_update)
            # self.real_item = self.clocks.real_clock.schedule_item(self.real_update, 1/30.0)
            # self.clocks.real_clock.schedule_item(self.draw, 1/1000.0)
            # self.clocks.real_clock.schedule_item(self.get_events, 1/30.0)
            # self.clocks.real_clock.schedule_item(self.per_second, 1)
            
            self.real_up_interval = 1000.0/30.0
            realtime_scheduler.schedule(self.real_update, self.real_up_interval)
            
            self.game_up_interval =  1000.0/60.0
            gametime_scheduler.schedule(self.game_update, self.game_up_interval)
            lockstep_scheduler.schedule(self.lockstep_update, self.game_up_interval)
            
            self.draw_interval = 1000.0 / 60.0
            realtime_scheduler.schedule(self.draw, self.draw_interval)
            self.event_interval = 1000.0/30.0
            realtime_scheduler.schedule(self.get_events, self.event_interval)
            self.second_interval = 1000.0
            realtime_scheduler.schedule(self.per_second, self.second_interval)
            
            
            
            self.nticks = 0
            self.nupdates = 0
            self.ndraws = 0
            self.nevents = 0
            
            self.ball_game1 = Ball('####', Color(100,100,235), 5*480/20)
            self.ball_game2 = Ball('####', Color(255,255,0), 6*480/20)
            self.ball_real = Ball('####', Color(255,0,0), 7*480/20)
            self.ball_lockstep = Ball('####', Color(0,255,0), 8*480/20)
            
            self.msgs = []
            self.txts = []
            
            self.running = False
        def run(self):
            self.running = True
            while self.running:
                frame_clock.update()
                
        
        def tick(self, n):
            self.nticks += 1
        
        def game_update(self, clk, gdt, gt):
            self.nupdates += 1
           
            # print '???', clk, gdt, gt
            # self.ball_game1.update(gdt/1000.0)
            # self.ball_game2.update(gdt/1000.0)
            self.ball_game1.update(self.game_up_interval/1000.0)
            self.ball_game2.update(self.game_up_interval/1000.0)
            return self.game_up_interval
        
        def real_update(self, clk, dt, t):
            # dt = t - self.ball_real.time
            self.ball_real.update(dt/1000.0)
            self.ball_real.update(self.real_up_interval/1000.0)
            # self.ball_real.time = t
            return self.real_up_interval
        
        def lockstep_update(self, clk, gdt, gt):
            print '+++++', clk, gdt, gt
            # self.ball_lockstep.update(gdt/1000.0)
            self.ball_lockstep.update(self.game_up_interval/1000.0)
            return self.game_up_interval
        
        def draw(self, clk, dt, t):
            self.ndraws += 1
            
            self.screen.fill((0,0,0))
            blit = self.screen.blit
            
            # No alpha
            blit(self.ball_game1.image, self.ball_game1.rect)
            
            # Alpha with game clock
            alpha = gametime_clock.alpha
            blit(self.ball_game2.image, self.ball_game2.rect_interp(alpha))
            
            # Alpha with real clock
            alpha = 0.0 #(time.time() - self.ball_real.time) / self.real_item.interval
            blit(self.ball_real.image, self.ball_real.rect_interp(alpha))
            
            # if self.which_clock < 3:
            # Alpha with lockstep clock
            # alpha = self.lock_item.accumulator / self.lock_item.interval
            alpha = lockstep_clock.alpha
            blit(self.ball_lockstep.image, self.ball_lockstep.rect_interp(alpha))
            
            if self.txts:
                y = 0
                h = self.txts[0].get_height()
                blit = self.screen.blit
                for txt in self.txts:
                    blit(txt, (0,y))
                    y += h
            pygame.display.flip()
            return self.draw_interval
        
        def get_events(self, clk, dt, t):
            self.nevents += 1
            
            for e in pygame.event.get():
                if e.type == KEYDOWN:
                    if e.key == K_ESCAPE:
                        self.running = False
                    elif e.key == K_d:
                        if e.mod & KMOD_SHIFT:
                            gametime_clock.dilatation -= 0.2
                        else:
                            gametime_clock.dilatation += 0.2
                elif e.type == QUIT:
                    self.running = False
            return self.event_interval
        
        def per_second(self, clk, dt, t):
            self.msgs = [
                time.asctime(time.localtime(t)),
                'real time {0:f}'.format(t),
                'ticks / sec {0:d}'.format(self.nticks),
                'updates / sec {0:d}'.format(self.nupdates),
                'draws / sec {0:d}'.format(self.ndraws),
                'events / sec {0:d}'.format(self.nevents),
                # 'dilation {0:0.1f}'.format(self.clocks.game_clock.dilation),
                'dilation {0:0.1f}'.format(gametime_clock.dilatation),
            ]
            
            render = self.font.render
            color = Color('white')
            self.txts = [render(msg, True, color) for msg in self.msgs]
            
            self.nticks = 0
            self.nupdates = 0
            self.ndraws = 0
            self.nevents = 0
            return self.second_interval
    
    pygame.init()
    Game().run()
#  -----------------------------------------------------------------------------
    